package engine

import (
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"gitee.com/zfd81/dbrs/sys"

	"gitee.com/zfd81/dbrs/errs"

	"gitee.com/zfd81/dbrs/sql/parser"

	"github.com/sirupsen/logrus"
)

type key uint

type (
	//// TypedValue is a value along with its type.
	//TypedValue struct {
	//	Typ   sql.Type
	//	Value interface{}
	//}

	// Warning stands for mySQL warning record.
	Warning struct {
		Level   string
		Message string
		Code    int
	}
)

const (
	// QueryKey to access query in the context.
	QueryKey key = iota
)

const (
	RowCount     = "row_count"
	FoundRows    = "found_rows"
	LastInsertId = "last_insert_id"
)

// Client holds session user information.
type Client struct {
	// User of the session.
	User string
	// Address of the client.
	Address string
	// Capabilities of the client
	Capabilities uint32
}

func defaultLastQueryInfo() map[string]int64 {
	return map[string]int64{
		RowCount:     0,
		FoundRows:    1, // this is kind of a hack -- it handles the case of `select found_rows()` before any select statement is issued
		LastInsertId: 0,
	}
}

// Session holds the session data.
type Session interface {
	// Address of the server.
	Address() string

	// Client returns the user of the session.
	Client() Client
	// SetSessionVariable sets the given sys variable to the value given for this session.
	SetSessionVariable(ctx *Context, sysVarName string, value interface{}) error
	// SetUserVariable sets the given user variable to the value given for this session, or creates it for this session.
	SetUserVariable(varName string, value interface{}) error
	// GetSessionVariable returns this session's value of the sys variable with the given name.
	GetSessionVariable(ctx *Context, sysVarName string) (interface{}, error)
	// GetUserVariable returns this session's value of the user variable with the given name, along with its most
	// appropriate type.
	GetUserVariable(ctx *Context, varName string) (interface{}, error)
	// GetAllSessionVariables returns a copy of all session variable values.
	GetAllSessionVariables() map[string]interface{}
	// GetAllEnvironmentVariables returns a copy of all session and user variable values.
	GetAllEnvironmentVariables() map[string]interface{}
	// GetCurrentDatabase gets the current database for this session
	GetCurrentDatabase() Provider
	// SetCurrentDatabase sets the current database for this session
	SetCurrentDatabase(dbName string)
	HasDatabase(name string) bool
	GetAllUserVariables() map[string]interface{}
	// ID returns the unique ID of the connection.
	ID() uint32
	// Warn stores the warning in the session.
	Warn(warn *Warning)
	// Warnings returns a copy of session warnings (from the most recent).
	Warnings() []*Warning
	// ClearWarnings cleans up session warnings.
	ClearWarnings()
	// WarningCount returns a number of session warnings
	WarningCount() uint16
	// AddLock adds a lock to the set of locks owned by this user which will need to be released if this session terminates
	AddLock(lockName string) error
	// DelLock removes a lock from the set of locks owned by this user
	DelLock(lockName string) error
	// IterLocks iterates through all locks owned by this user
	IterLocks(cb func(name string) error) error
	// SetLastQueryInfo sets session-level query info for the key given, applying to the query just executed.
	SetLastQueryInfo(key string, value int64)
	// GetLastQueryInfo returns the session-level query info for the key given, for the query most recently executed.
	GetLastQueryInfo(key string) int64

	// GetLogger returns the logger for this session, useful if clients want to log messages with the same format / output
	// as the running server. Clients should instantiate their own global logger with formatting options, and session
	// implementations should return the logger to be used for the running server.
	GetLogger() *logrus.Entry
	// SetLogger sets the logger to use for this session, which will always be an extension of the one returned by
	// GetLogger, extended with session information
	SetLogger(*logrus.Entry)

	CreateTime() time.Time
}

// BaseSession is the basic session type.
type BaseSession struct {
	mu            sync.RWMutex
	id            uint32
	addr          string
	client        Client
	currentDB     string
	systemVars    map[string]interface{}
	userVars      map[string]interface{}
	envVars       map[string]interface{}
	warnings      []*Warning
	warncnt       uint16
	locks         map[string]bool
	queriedDb     string
	lastQueryInfo map[string]int64
	ps            map[string]Provider
	logger        *logrus.Entry
	createTime    time.Time
}

func (s *BaseSession) GetLogger() *logrus.Entry {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.logger == nil {
		log := logrus.StandardLogger()
		s.logger = logrus.NewEntry(log)
	}
	return s.logger
}

func (s *BaseSession) SetLogger(logger *logrus.Entry) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.logger = logger
}

var _ Session = (*BaseSession)(nil)

// Address returns the server address.
func (s *BaseSession) Address() string { return s.addr }

// Client returns session's client information.
func (s *BaseSession) Client() Client { return s.client }

// GetAllSessionVariables implements the Session interface.
func (s *BaseSession) GetAllSessionVariables() map[string]interface{} {
	m := make(map[string]interface{})
	s.mu.RLock()
	defer s.mu.RUnlock()

	for k, v := range s.systemVars {
		m[k] = v
	}
	return m
}

func (s *BaseSession) SetSessionVariable(ctx *Context, sysVarName string, value interface{}) error {
	_, ok := sys.SystemVariables.GetGlobal(sysVarName)
	if !ok {
		return errs.ErrUnknownSystemVariable.New(sysVarName)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.systemVars[sysVarName] = value
	return nil
}

// SetUserVariable implements the Session interface.
func (s *BaseSession) SetUserVariable(varName string, value interface{}) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.userVars[strings.ToLower(varName)] = value
	return nil
}

// GetSessionVariable implements the Session interface.
func (s *BaseSession) GetSessionVariable(ctx *Context, sysVarName string) (interface{}, error) {
	_, ok := sys.SystemVariables.GetGlobal(sysVarName)
	if !ok {
		return nil, errs.ErrUnknownSystemVariable.New(sysVarName)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	val, _ := s.systemVars[strings.ToLower(sysVarName)]
	return val, nil
}

// GetUserVariable implements the Session interface.
func (s *BaseSession) GetUserVariable(ctx *Context, varName string) (interface{}, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	val, ok := s.userVars[strings.ToLower(varName)]
	if !ok {
		return nil, nil
	}
	return val, nil
}

// GetCurrentDatabase gets the current database for this session
func (s *BaseSession) GetCurrentDatabase() Provider {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.ps[s.currentDB]
}

func (s *BaseSession) GetAllUserVariables() map[string]interface{} {
	m := make(map[string]interface{})
	s.mu.RLock()
	defer s.mu.RUnlock()

	for k, v := range s.userVars {
		m[k] = v
	}
	return m
}

func (s *BaseSession) GetAllEnvironmentVariables() map[string]interface{} {
	return s.envVars
}

// SetCurrentDatabase sets the current database for this session
func (s *BaseSession) SetCurrentDatabase(dbName string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.currentDB = dbName
}

func (s *BaseSession) HasDatabase(name string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, ok := s.ps[name]
	return ok
}

// ID implements the Session interface.
func (s *BaseSession) ID() uint32 { return s.id }

// Warn stores the warning in the session.
func (s *BaseSession) Warn(warn *Warning) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.warnings = append(s.warnings, warn)
}

// Warnings returns a copy of session warnings (from the most recent - the last one)
// The function implements sql.Session interface
func (s *BaseSession) Warnings() []*Warning {
	s.mu.RLock()
	defer s.mu.RUnlock()

	n := len(s.warnings)
	warns := make([]*Warning, n)
	for i := 0; i < n; i++ {
		warns[i] = s.warnings[n-i-1]
	}

	return warns
}

// ClearWarnings cleans up session warnings
func (s *BaseSession) ClearWarnings() {
	s.mu.Lock()
	defer s.mu.Unlock()

	cnt := uint16(len(s.warnings))
	if s.warncnt == cnt {
		if s.warnings != nil {
			s.warnings = s.warnings[:0]
		}
		s.warncnt = 0
	} else {
		s.warncnt = cnt
	}
}

// WarningCount returns a number of session warnings
func (s *BaseSession) WarningCount() uint16 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return uint16(len(s.warnings))
}

// AddLock adds a lock to the set of locks owned by this user which will need to be released if this session terminates
func (s *BaseSession) AddLock(lockName string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.locks[lockName] = true
	return nil
}

// DelLock removes a lock from the set of locks owned by this user
func (s *BaseSession) DelLock(lockName string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	delete(s.locks, lockName)
	return nil
}

// IterLocks iterates through all locks owned by this user
func (s *BaseSession) IterLocks(cb func(name string) error) error {
	s.mu.RLock()
	defer s.mu.RUnlock()

	for name := range s.locks {
		err := cb(name)

		if err != nil {
			return err
		}
	}

	return nil
}

// GetQueriedDatabase implements the Session interface.
func (s *BaseSession) GetQueriedDatabase() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.queriedDb
}

// SetQueriedDatabase implements the Session interface.
func (s *BaseSession) SetQueriedDatabase(dbName string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.queriedDb = dbName
}

func (s *BaseSession) SetLastQueryInfo(key string, value int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.lastQueryInfo[key] = value
}

func (s *BaseSession) GetLastQueryInfo(key string) int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.lastQueryInfo[key]
}

func (s *BaseSession) CreateTime() time.Time {
	return s.createTime
}

// NewSession creates a new session with data.
func NewSession(server string, client Client, id uint32, providers map[string]Provider) Session {
	s := &BaseSession{
		mu:            sync.RWMutex{},
		addr:          server,
		client:        client,
		id:            id,
		systemVars:    sys.SystemVariables.NewSessionMap(),
		userVars:      make(map[string]interface{}),
		locks:         make(map[string]bool),
		lastQueryInfo: defaultLastQueryInfo(),
		ps:            providers,
		createTime:    time.Now(),
	}
	s.envVars = map[string]interface{}{
		parser.Sys_Variable_Prefix:  s.systemVars,
		parser.User_Variable_Prefix: s.userVars,
	}
	return s
}

// Session ID 0 used as invalid SessionID
var autoSessionIDs uint32 = 1

// NewBaseSession creates a new empty session.
func NewBaseSession() Session {
	return &BaseSession{
		mu:            sync.RWMutex{},
		id:            atomic.AddUint32(&autoSessionIDs, 1),
		systemVars:    sys.SystemVariables.NewSessionMap(),
		userVars:      make(map[string]interface{}),
		locks:         make(map[string]bool),
		lastQueryInfo: defaultLastQueryInfo(),
	}
}
